package core;

import java.lang.reflect.Field;

import net.ByteBuffer;
import net.ByteReader;
import util.Logger;

/**
 * 
 * The base class of all game entities.
 * 
 * All entities support cloning and custom serialization.
 * 
 * @author Jason
 * 
 */
public abstract class Entity implements Cloneable {

	private static int ENTITY_COUNT = 0;

	private static final int POSITION_THRESHOLD = 5;
	private static final float VELOCITY_MAGNITUDE_THRESHOLD = .005f;
	private static final float VELOCITY_HEADING_THRESHOLD = .01f;
	private static final float ROTATION_SPEED_THRESHOLD = .001f;

	/**
	 * The Unique ID of this entity.
	 */
	public int id;

	/**
	 * The location of this entity.
	 */
	public float x, y;

	/**
	 * The magnitude of the velocity of this entity.
	 */
	public float velocityMagnitude;

	/**
	 * The heading of the velocity of this entity.
	 */
	public float velocityHeading;

	/**
	 * The speed that this entity is turning.
	 */
	public float rotationSpeed;

	protected Entity() {
		id = ENTITY_COUNT++;
	}

	/**
	 * Writes this Entity as a series of bytes into the ByteBuffer.
	 */
	public final void writeEntity(ByteBuffer bb) {
		bb.addInt(id);
		bb.addString(getChildClass().getName());
		writeAttributes(bb);
	}

	/**
	 * Writes the attributes of this entity to the byte buffer. As a general
	 * rule, classes overriding this method should call
	 * super.writeAttributes(buffer) as the first line in the method.
	 */
	protected void writeAttributes(ByteBuffer buffer) {
		buffer.addInt((int) x);
		buffer.addInt((int) y);
		buffer.addFloat(velocityMagnitude);
		buffer.addFloat(velocityHeading);
		buffer.addFloat(rotationSpeed);
	}

	/**
	 * Reads the attributes of this entity from the byte reader. As a general
	 * rule, classes overriding this method should call
	 * super.readAttributes(reader) as the first line in the method.
	 * 
	 * Note: All field copies should be <b>deep copies</b>.
	 */
	public void readAttributes(ByteReader reader) {
		x = reader.readInt();
		y = reader.readInt();
		velocityMagnitude = reader.readFloat();
		velocityHeading = reader.readFloat();
		rotationSpeed = reader.readFloat();
	}

	/**
	 * Copies the attributes of the given entity onto this one.
	 * 
	 * As a general rule, class overriding this method should call
	 * super.readAttributes(e) as the first line in the method.
	 * 
	 * @param e
	 */
	protected void readAttributes(Entity e) {
		x = e.x;
		y = e.y;
		velocityMagnitude = e.velocityMagnitude;
		velocityHeading = e.velocityHeading;
		rotationSpeed = e.rotationSpeed;
	}

	/**
	 * Note that this is an APPROXIMATE equals. All comparisons should be made
	 * with appropriate thresholds.
	 * 
	 * Code should look like this: if(Math.abs(myVal-e.myVal) >
	 * MYVAL_THRESHOLD)) return false;
	 * 
	 * This method is used by the server to determine when it needs to send
	 * update packets. The lower the threshold, the more update packets, but the
	 * higher the bandwidth cost.
	 * 
	 * As a general rule, class overriding this method should call
	 * if(super.equals(e) == false) return false; as the first line in the
	 * method.
	 * 
	 * @param e
	 *            The entity to compare with.
	 * 
	 * @return True if the given entity is approximately equal to this one.
	 */
	public boolean equals(Entity e) {
		return id == e.id
				&& Math.abs(x - e.x) < POSITION_THRESHOLD
				&& Math.abs(y - e.y) < POSITION_THRESHOLD
				&& Math.abs(velocityMagnitude - e.velocityMagnitude) < VELOCITY_MAGNITUDE_THRESHOLD
				&& Math.abs(velocityHeading - e.velocityHeading) < VELOCITY_HEADING_THRESHOLD
				&& Math.abs(rotationSpeed - e.rotationSpeed) < ROTATION_SPEED_THRESHOLD;
	}

	@Override
	public boolean equals(Object o) {
		if (o instanceof Entity)
			return equals((Entity) o);
		return false;
	}

	/**
	 * Ticks this entity.
	 * 
	 * @param seconds
	 *            The amount of time passed.
	 */
	public void tick(double seconds) {
		velocityHeading += rotationSpeed * seconds;
		x += Math.cos(velocityHeading) * velocityMagnitude * seconds;
		y += -Math.sin(velocityHeading) * velocityMagnitude * seconds;
	}

	/**
	 * Creates a copy of this entity
	 */
	@Override
	public Entity clone() {
		try {
			Entity clone = (Entity) super.clone();
			return clone;
		} catch (CloneNotSupportedException e) {
			throw new InternalError(e.toString());
		}
	}

	/**
	 * Used for displaying nice debugging output using reflection.
	 */
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		Class<?> current = getChildClass();
		while (true) {
			sb.append(current.getName()).append('\n');
			Field[] fields = current.getDeclaredFields();
			for (Field f : fields) {
				f.setAccessible(true);
				try {
					sb.append(f.getName()).append("-->").append(f.get(this))
							.append('\n');
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
			current = current.getSuperclass();
			if (current == Object.class)
				break;
		}
		return sb.toString();
	}

	/**
	 * All subclasses of entity should have this code:
	 * 
	 * public Class<?> getChildClass(){ return this.getClass(); }
	 * 
	 */
	public abstract Class<?> getChildClass();

	/**
	 * Constructs an entity from the reader's bytes. The buffer should be set in
	 * a position such that the first thing read is the entity's ID.
	 */
	@SuppressWarnings("unchecked")
	public static Entity create(ByteReader br) {
		int id = br.readInt();
		String className = br.readString();
		Class<Entity> theClass;
		Entity created;
		try {
			theClass = (Class<Entity>) Class.forName(className);
		} catch (ClassNotFoundException e) {
			Logger.log(e);
			return null;
		}
		try {
			created = theClass.newInstance();
		} catch (Exception e) {
			Logger.log(Logger.SEVERE, className
					+ " must supply an empty constructor.");
			return null;
		}
		created.id = id;
		created.readAttributes(br);
		return created;
	}

}
